#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author  : Leo.W
# @Date    : 2024/1/24 10:19
# @File    : 6-function.py
# @Software: PyCharm

"""
定义函数:
定义函数使用关键字 def，后跟 <函数名> 与括号内的形参列表，命名函数与方法用 lowercase_with_underscores。格式如下：
def function_name(arg1, arg2,...):
    '''函数注释'''
    ...<函数体>
    return <返回值>

函数的参数列表：
  默认情况下，参数可以按位置或显式关键字传递给 Python 函数。
  为了让代码易读、高效，最好限制参数的传递方式，开发者只需查看函数定义，即可确定参数项是仅按位置、按位置或关键字，还是仅按关键字传递。
  参数可以设置默认值，使用 name=value 的形式。

函数的返回值：
  return 语句返回函数的值。return 语句不带表达式参数时，返回 None。函数执行完毕退出也返回 None。

函数的注释：
  函数内的第一条语句是字符串时(使用三个双引号或单引号扩起来)，该字符串就是文档字符串(docstring)
"""
# --------------------------- 函数定义示例 ---------------------------


def ask_ok(prompt, retries=4, reminder="Please try again!"):
    """
    函数示例：为参数指定默认值
    :param prompt: 命名参数
    :param retries: 尝试次数，默认值为 4
    :param reminder: 提示信息，默认值为 "Please try again!"
    :return:None
    """
    while True:
        reply = input(prompt)
        if reply in {"y", "ye", "yes"}:  # 使用关键字 in =>用于确认序列中是否包含某个值
            return True
        if reply in {"n", "no", "nop", "nope"}:
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError("invalid user response")
        print(reminder)


# 只给出必选实参：
# ask_ok("Do you really want to quit?")

# 给出一个可选实参：
# ask_ok("OK to overwrite the file?", 2)

# 给出所有实参：
# ask_ok("OK to overwrite the file?", 2, "Come on, only yes or no!")


# --------------------------- 位置或关键字参数 ---------------------------
# 函数定义中未使用 / 和 * 时，参数可以按位置或关键字传递给函数。
def key_arg_test(arg1, arg2="ARG2", arg3=True, arg4="OK"):
    """
    函数调用时使用关键字参数测试。
    :param arg1: 必选参数
    :param arg2: 非必选参数，默认值 "ARG2"
    :param arg3: 非必选参数，默认值 True
    :param arg4: 非必选参数，默认值 "OK"
    :return: None
    """
    print(f"arg1={arg1}, arg2={arg2}, arg3={arg3}, arg4={arg4}")


# 不指定关键字，则函数根据参数次序进行匹配
key_arg_test(100)  # 1 positional argument
key_arg_test("Holle", "world", False)  # 3 positional arguments

# 指定关键字参数 key=value 的模式，key 要与 参数列表中的参数名对应，位置顺序不重要
key_arg_test(arg1=200)  # 1 keyword argument
key_arg_test(arg2="Arg2 at first", arg1=200)  # 2 keyword arguments
key_arg_test(300, arg4="HAHAHA")  # 1 positional AND 1 keyword argument
# 注意：不能对同一个参数多次赋值
# TypeError: key_arg_test() got multiple values for argument 'arg1'
# key_arg_test(10, arg1=200)


# 重要警告： 默认值只计算一次。默认值为列表、字典或类实例等可变对象时，会产生与该规则不同的结果
def variadic_object_args(arg, L=[]):
    """
    参数中包含默认值为列表、字典或类实例等可变对象
    :param arg:
    :param L: 默认值为空列表的参数
    :return: List
    """
    L.append(arg)  # 每次函数调用参数L都会改变：积累参数 arg
    return L


# 因为默认值参数 L 仅计算一次，所以第一次调用后为 L 分配了内存空间地址不会变动(在后续调用之间会共享默认值)。当再次调用时只会为 L 中增加元素，不会重置为 []
print(variadic_object_args(1))  # [1]
print(variadic_object_args(2))  # [1, 2]
print(variadic_object_args(3))  # [1, 2, 3]
# 避免上述问题，应该在调用时给 L 指定新的值
print(variadic_object_args(4, []))  # [4]


# 或 改良写法：
def variadic_object_args_v2(arg, L=None):  # 初始设置 L 为 None
    if L is None:
        L = []  # 每次调用时重新给L分配内存空间，在后续调用之间不共享默认值
    L.append(arg)
    return L


# --------------------------- 特殊参数 ---------------------------
"""
特殊参数:

def function_name(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
                  -----------    ----------     ----------
                    |                |               |
                    |        Positional or keyword   |
                    |                                +-- Keyword only
                    +-- Positional only
    ...<函数体>
    return <返回值>

函数的参数列表：
  默认情况下，参数可以按位置或显式关键字传递给 Python 函数。
  为了让代码易读、高效，最好限制参数的传递方式，开发者只需查看函数定义，即可确定参数项是仅按位置、按位置或关键字，还是仅按关键字传递。

  - / 和 * 是可选的：
    + 这些符号表明形参如何把参数值传递给函数：位置、位置或关键字、关键字。关键字形参也叫作命名形参。
    + 函数定义中未使用 / 和 * 时，参数可以按位置或关键字传递给函数。
  - 可变参数：最后一个形参为 **name 形式时，接收一个字典（映射类型 dict），该字典包含与函数中已定义形参对应之外的所有关键字参数。
    **name 形参可以与 *name 形参组合使用（*name 必须在 **name 前面）， *name 形参接收一个 元组，该元组包含形参列表之外的位置参数。
"""


# ------------ 仅位置参数 ------------
# / 用于在逻辑上分割仅限位置形参与其它形参。
# 如果函数定义中没有 /，则表示没有仅限位置形参。
# / 后可以是 位置或关键字 或 仅限关键字 形参。
def pos_only_arg(arg, /):
    """
    仅位置参数函数示例
    :param arg: 仅限位置形参，放在 / （正斜杠）前
    :return:
    """
    print(arg)


# pos_only_arg(1)
# TypeError: pos_only_arg() got some positional-only arguments passed as keyword arguments: 'arg'
# pos_only_arg(arg=1)  # 仅位置参数，函数调用时不允许使用 关键字 传参


# ------------ 仅限关键字参数 ------------
# 应在参数列表中第一个 仅限关键字 形参前添加 *。
# *,之后的参数仅限 关键字 形式传值
def kwd_only_arg(*, arg):
    print(arg)


# kwd_only_arg(arg="Hello")
# TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given
# kwd_only_arg("Hello")


def combined_example(pos_only, /, standard, *, kwd_only):
    """
    位置参数和关键字参数混合使用示例
    :param pos_only: 仅位置参数，在 / 之前
    :param standard: 可以时位置参数 或 关键字参数
    :param kwd_only: 仅关键字参数，在 * 之后
    :return:
    """
    print(pos_only, standard, kwd_only)


# combined_example("positional", "standard-positional", kwd_only="keyword")
# combined_example("positional", standard="standard-keyword", kwd_only="keyword")


# ------------ 任意长度实参列表 ------------
"""
使用 *arguments 的形式表示可变数量的实参，用于采集传递给函数的所有剩余参数，这些实参包含在元组中。
它们通常在形参列表的末尾。*arguments 形参后的任何形式参数只能是仅限关键字参数，即只能用作关键字参数，不能用作位置参数

使用 **keywords 的形式用于采集传递给函数的所有剩余【关键字参数】：
    **keywords 可以在 *arguments 之后与之配合使用
    **keywords 采集的参数存贮在一个字典中
"""


def variadic_arguments(kind, *arguments, **keywords):
    """
    可变数量参数示例
    :param kind: 常规参数
    :param arguments: 可变数量参数（元组）
    :param keywords: 可变数量的关键字参数（字典）
    :return:
    """
    print("kind -> ", kind)
    print("-------- *arguments --------")
    for arg in arguments:
        print(arg)
    print("-------- **keywords --------")
    for kw in keywords:
        print(kw, ":", keywords[kw])


# variadic_arguments("Hello", "a", "b", "C", name="Tom", age=18, gender="M")

"""
kind ->  Hello
-------- *arguments --------
a
b
C
-------- **keywords --------
name : Tom
age : 18
gender : M
"""


"""
用 * 操作符把实参从列表或元组解包出来，主要针对位置参数使用
用 ** 操作符把实参从字典中解包出来，用作传递关键字参数
"""
args = (100, "ARG2", False, "Hello world")
key_arg_test(*args)

dic = {"arg1": 200, "arg2": "ARG2", "arg3": True, "arg4": "OK"}
key_arg_test(**dic)

# --------------------------- Lambda 表达式 ---------------------------
# lambda 关键字用于创建小巧的【匿名函数】。例如：lambda a, b: a+b 函数返回两个参数的和。
# Lambda 函数可用于任何需要【函数对象】的地方。
#   在语法上，匿名函数只能是单个表达式。
#   在语义上，它只是常规函数定义的语法糖。


def make_incrementor(n):
    """
    Lambda 函数示例
    :param n:
    :return: 匿名函数
    """
    return lambda x: x + n  # 用 lambda 表达式返回函数


f = make_incrementor(10)
print(f(1))  # 11


# 还可以把匿名函数用作传递的实参：
pairs = [(1, "one"), (2, "two"), (3, "three"), (4, "four")]
pairs.sort(key=lambda pair: pair[1])
print(pairs)
